// a level in the game, also known as a board

#include <stdlib.h>
#include <iostream>
#include <fstream>

#include "board.h"
#include "soundManager.h"

static const int MAX_BOARD_WIDTH = 12;
static const int MAX_BOARD_HEIGHT = 12;

// only get 99 charges
static const int MAX_NUM_POWER_USE = 99;

// delimits fields in the file format
static const int DELIMITER = 999;

// returns the next number in the stream and chomps the comma
static
int getNum(ifstream &in) {
	int i;
	char c;

	in >> i;
	in >> c;	// chomp comma

	return i;
}

static
void chompDelim(ifstream &in) {
	int i = getNum(in);
	assert(i == DELIMITER);
}

// checks if x, y, and o are in bounds and
// if the tile (x, y) is OK to stand on
static
void checkBounds(int x, int y, Facing o, Board *b) {
	// checks for in-bounds ness
	TILE_TYPE tile = b->getTile(x, y);

	bool ok = o >= FA_N && o <= FA_W &&
			  tile != WALL && tile != WATER && tile != EXIT;
	if (!ok) {
		printf("character not standing on an OK tile: (%d, %d)\n", x, y);
		exit(0);
	}
}

// checks if the power count is in bounds
static
void checkBoundsPower(int n) {
	bool ok = (n >= 0 && n <= MAX_NUM_POWER_USE);
	if (!ok) {
		printf("power usage out of bounds: %d\n", n);
		exit(0);
	}
}

Board::Board(char *filename)
	: ill(0, 0, FA_S)
{
	ifstream in;
	in.open(filename);

	int input_i;

	width = getNum(in);
	height = getNum(in);

	assert(width > 0 && width <= MAX_BOARD_WIDTH);
	assert(height > 0 && height <= MAX_BOARD_HEIGHT);

	// width and height are known, now load the tiles
	tiles = (TILE_TYPE *)malloc(sizeof(TILE_TYPE) * width * height);
	for (int j = 0; j < height; j++) {
		for (int i = 0; i < width; i++) {
			input_i = getNum(in);

			assert(input_i >= FLOOR && input_i <= EXIT);
			tiles[j*width + i] = (TILE_TYPE)input_i;
//			printf("%d, ", input_i);
		}
//		printf("\n");
	}

	chompDelim(in);

	int x;
	int y;
	Facing o;

	// create illusionist
	x = getNum(in);
	y = getNum(in);
	o = (Facing)getNum(in);

	checkBounds(x, y, o, this);
	ill = Ill(x, y, o);

	chompDelim(in);

	// create guards
	guards.clear();
	while (true) {
		x = getNum(in);
		if (x == DELIMITER) {
			break;
		}
		y = getNum(in);
		o = (Facing)getNum(in);

		checkBounds(x, y, o, this);
		guards.push_back(Guard(x, y, o));
	}

	// create dogs
	dogs.clear();
	while (true) {
		x = getNum(in);
		if (x == DELIMITER) {
			break;
		}
		y = getNum(in);
		o = (Facing)getNum(in);

		checkBounds(x, y, o, this);
		dogs.push_back(Dog(x, y, o));
	}

	// create super guards
	sgs.clear();
	while (true) {
		x = getNum(in);
		if (x == DELIMITER) {
			break;
		}
		y = getNum(in);
		o = (Facing)getNum(in);

		checkBounds(x, y, o, this);
		sgs.push_back(SG(x, y, o));
	}

	// load how many power usages you have
	input_i = getNum(in);
	checkBoundsPower(input_i);
	numFood = input_i;

	input_i = getNum(in);
	checkBoundsPower(input_i);
	numDeadBody = input_i;

	input_i = getNum(in);
	checkBoundsPower(input_i);
	numCloak = input_i;

	input_i = getNum(in);
	checkBoundsPower(input_i);
	numFake = input_i;

	in.close();

	winBit = false;
	loseBit = false;
}

Board::~Board() {
	free(tiles);
}

//--- ILLUSIONIST MOVEMENT ---//
void Board::move(int dx, int dy) {
	// figure out the new orientation
	if (dx > 0) {
		ill.o = FA_E;
	} else if (dx < 0) {
		ill.o = FA_W;
	} else if (dy > 0) {
		ill.o = FA_S;
	} else if (dy < 0) {
		ill.o = FA_N;
	}

	int nx = ill.x + dx;
	int ny = ill.y + dy;

	// do bounds check, no update if dst is out of bounds
	if (nx < 0 || nx >= width ||
		ny < 0 || ny >= height)
	{
		return;
	}

	TILE_TYPE dstTile = getTile(nx, ny);

	// can't walk through walls or on water
	if (dstTile == WALL || dstTile == WATER) {
		return;
	}

	// check to see if a person is standing in this location,
	// this will lead to a loss
	Actor *actor;
	ObjectType actorType = getObject(nx, ny, actor);

	if (actorType == OB_GUARD || actorType == OB_DOG || actorType == OB_SG) {
		setLose();
	}

	// update to the new location
	ill.x = nx;
	ill.y = ny;
}

void Board::moveUp() {
	if (ill.o == FA_N) move(0, -1);
	else ill.o = FA_N;
}

void Board::moveDown() {
	if (ill.o == FA_S) move(0, 1);
	else ill.o = FA_S;
}

void Board::moveLeft() {
	if (ill.o == FA_W) move(-1, 0);
	else ill.o = FA_W;
}

void Board::moveRight() {
	if (ill.o == FA_E) move(1, 0);
	else ill.o = FA_E;
}

//--- ILLUSIONIST POWERS ---//

// decrement the counter but keep it at 0
static void decClamped(int &i) {
	i = max(0, i - 1);
}

// return the tile that the illusionist is looking at,
// if the illusionist is looking out of bounds, return WALL,
// also sets dstX and dstY to the tile the illusionist is
// looking at:
// NOTE if he is looking out of bounds, the out params
// may also be out of bounds
TILE_TYPE getIllFacingTile(Board *b, int &x, int &y) {
	Ill &ill = b->getIll();
	x= ill.x;
	y = ill.y;

	if (ill.o == FA_N) y--;
	else if (ill.o == FA_S) y++;
	else if (ill.o == FA_E) x++;
	else if (ill.o == FA_W) x--;

	if (x < 0 || x >= b->getWidth() || y < 0 || y > b->getHeight()) {
		return WALL;
	}

	return b->getTile(x, y);
}

void Board::useFood() {
	if (numFood <= 0) {
		return;
	}

	int dstX, dstY;
	TILE_TYPE tile = getIllFacingTile(this, dstX, dstY);

	Actor *actor;
	ObjectType inTile = getObject(dstX, dstY, actor);

	// check if the space in front of you is clear for laying an illusion
	if ((tile == FLOOR) || (tile == IN_FLOOR)) {
		// make sure no actor inhabits space in front
		if (inTile == OB_EMPTY) {
			// drop food on space in front of ill
			foods.push_back(Food(dstX, dstY, FA_S));
			decClamped(numFood);
			playSfx(SFX_ILLUSION_SOUND);
		}
	}
}

void Board::useDeadBody() {
	if (numDeadBody <= 0) {
		return;
	}

	int dstX, dstY;
	TILE_TYPE tile = getIllFacingTile(this, dstX, dstY);

	Actor *actor;
	ObjectType inTile = getObject(dstX, dstY, actor);

	// check if the space in front of you is clear for laying an illusion
	if ((tile == FLOOR) || (tile == IN_FLOOR)) {
		// make sure no actor inhabits space in front
		if (inTile == OB_EMPTY) {
			// drop a dead body on space in front of ill
			deadBodies.push_back(DeadBody(dstX, dstY, FA_S));
			decClamped(numDeadBody);
			playSfx(SFX_ILLUSION_SOUND);
		}
	}
}

void Board::useCloak() {
	if (numCloak <= 0) {
		return;
	}

	// activate cloak
	ill.activateCloak();	// callback set within this method

	decClamped(numCloak);
	playSfx(SFX_ILLUSION_SOUND);
}

void Board::useFake() {
	if (numFake <= 0) {
		return;
	}

	// check if the space in front of you has an enemy
	int dstX, dstY;
	TILE_TYPE tile = getIllFacingTile(this, dstX, dstY);
	assert(tile == FLOOR || tile == IN_FLOOR);

	Actor *actor;
	ObjectType inTile = getObject(dstX, dstY, actor);

	if (inTile == OB_GUARD || inTile == OB_DOG || inTile == OB_SG) {
		// convert the enemy into a fake illusionist
		actor->makeFakeIll();
		decClamped(numFake);
		playSfx(SFX_ILLUSION_SOUND);
	}
}

//--- BOARD STATE ---//
void Board::checkForWin() {
	// win if the ill is on the exit square
	if (getTile(ill) == EXIT) {
		winBit = true;
	}
}

bool Board::hasLost() {
	return loseBit;
}

bool Board::hasWon() {
	// must have won and also not suffered a lose condition
	return winBit && !loseBit;
}

void Board::setLose() {
	ill.loseLife();
	loseBit = true;
}

void Board::resetWinLoss() {
	winBit = false;
	loseBit = false;
}

// returns the type of object and fills in a pointer
// to that object as an out param, if the coordinate
// are out of bounds, returns OBJ_EMPTY and NULL
ObjectType Board::getObject(int x, int y, Actor *(&obj)) {
	if (!inBounds(x, y)) {
		obj = NULL;
		return OB_EMPTY;
	}

	if (ill.isAt(x, y)) {
		obj = &ill;
		return OB_ILL;
	}

	for (unsigned int i = 0; i < guards.size(); i++) {
		if (guards[i].isAt(x, y)) {
			obj = &guards[i];
			return OB_GUARD;
		}
	}

	for (unsigned int i = 0; i < dogs.size(); i++) {
		if (dogs[i].isAt(x, y)) {
			obj = &dogs[i];
			return OB_DOG;
		}
	}

	for (unsigned int i = 0; i < sgs.size(); i++) {
		if (sgs[i].isAt(x, y)) {
			obj = &sgs[i];
			return OB_SG;
		}
	}

	for (unsigned int i = 0; i < foods.size(); i++) {
		if (foods[i].isAt(x, y)) {
			obj = &foods[i];
			return OB_FOOD;
		}
	}

	for (unsigned int i = 0; i < deadBodies.size(); i++) {
		if (deadBodies[i].isAt(x, y)) {
			obj = &deadBodies[i];
			return OB_DEADBODY;
		}
	}

	obj = NULL;
	return OB_EMPTY;
}



//--- GLOBALS ---//
static const char *levelPre = "assets/levels/level";
//static const char *levelPre = "assets/levels/debug_level";
static const char *levelPost = ".txt";
static Board *globalBoard = NULL;					// the active board
static int globalCurrLevel = 0;							// the current level the player is on


// current level's filename
static
char *getLevelFilename() {
	static const int BUF_LEN = 512;
	static char buf[BUF_LEN];

	assert(globalCurrLevel >= 0);
	sprintf(buf, "%s%02d%s", levelPre, globalCurrLevel, levelPost);
	return buf;
}

// uses the current level's filename to set the board,
// if the file doesn't exist, returns false
static
bool setCurrBoard() {
	delete globalBoard;

	char *filename = getLevelFilename();

	FILE *fp = fopen(filename, "r");
	if (fp == NULL) {
		globalBoard = NULL;
		return false;
	}
	fclose(fp);

	globalBoard = new Board(filename);

	return true;
}

//--- EXTERNAL INTERFACE ---//
void initBoards() {
	bool rv = setCurrBoard();
	assert(rv);
}

// returns the active board, returns NULL is there
// is no next board i.e. game has been beaten
Board *getCurrentBoard() {
	return globalBoard;
}

// loads the current board, useful for resetting the state
void loadCurrBoard(int illLives) {
	setCurrBoard();
	globalBoard->setIllLives(illLives);
}

// loads the next board
void loadNextBoard() {
	globalCurrLevel++;
	setCurrBoard();
}

// loads the first board
void loadInitialBoard() {
	globalCurrLevel = 0;
	setCurrBoard();
}

// end of board.h
